[id].astro raw

   1  ---
   2  export const prerender = false;
   3  import Base from '../../layouts/Base.astro';
   4  import { getOrderForView, orderView } from '../../lib/orders';
   5  
   6  const token = Astro.url.searchParams.get('t') || '';
   7  const order = getOrderForView(Astro.params.id!, token);
   8  if (!order) {
   9    return new Response(null, { status: 404, statusText: 'Not Found' });
  10  }
  11  const view = orderView(order);
  12  const statusLabels: Record<string, string> = {
  13    pending: 'Изчаква плащане',
  14    paid: 'Платена',
  15    failed: 'Неуспешно плащане',
  16    expired: 'Изтекла',
  17    shipped: 'Изпратена',
  18    delivered: 'Доставена',
  19    cancelled: 'Отказана',
  20  };
  21  const methodLabels: Record<string, string> = {
  22    stripe: 'Карта (Stripe)',
  23    lightning: 'Lightning (Bitcoin)',
  24    onchain: 'Bitcoin (onchain)',
  25    bank: 'Банков превод',
  26    cod: 'Наложен платеж',
  27  };
  28  ---
  29  <Base title="Поръчка">
  30    <section class="section">
  31      <div class="container">
  32        <h1>Поръчка <span class="order-id">{view.id.slice(0, 8)}</span></h1>
  33        <p class="order-status" data-status={view.status}>
  34          Статус: <strong>{statusLabels[view.status] || view.status}</strong>
  35        </p>
  36  
  37        <div class="order-items">
  38          {view.items.map((item: any) => (
  39            <div class="order-item">
  40              <span>{item.name} × {item.qty} бр.</span>
  41              <span>{((item.price_cents * item.qty) / 100).toFixed(2)} EUR</span>
  42            </div>
  43          ))}
  44          <div class="order-item order-sub">
  45            <span>Междинна сума</span>
  46            <span>{(view.subtotal_cents / 100).toFixed(2)} EUR</span>
  47          </div>
  48          <div class="order-item order-sub">
  49            <span>Доставка</span>
  50            <span>{view.shipping_cents === 0 ? 'Безплатна' : (view.shipping_cents / 100).toFixed(2) + ' EUR'}</span>
  51          </div>
  52          <div class="order-item order-total">
  53            <strong>Общо</strong>
  54            <strong>{(view.total_cents / 100).toFixed(2)} EUR</strong>
  55          </div>
  56        </div>
  57  
  58        <h2>Плащане: {methodLabels[view.payment_method] || view.payment_method}</h2>
  59        <div id="payment-area" data-order={view.id} data-token={token} data-method={view.payment_method} data-status={view.payment_status}>
  60          {view.payment_method === 'bank' && view.status === 'pending' && (
  61            <p>Ще получите банковите данни за превод на посочения имейл. Поръчката се обработва след постъпване на сумата.</p>
  62          )}
  63          {view.payment_method === 'cod' && (
  64            <p>Плащате в брой при получаване на пратката.</p>
  65          )}
  66          {view.payment_method === 'lightning' && view.status === 'pending' && view.payment?.pr && (
  67            <div class="ln-invoice">
  68              <p>Платете Lightning фактурата с вашия портфейл:</p>
  69              <code class="invoice-code">{view.payment.pr}</code>
  70              <div style="margin-top:0.75rem">
  71                <a class="btn btn-primary" href={`lightning:${view.payment.pr}`}>Отвори в портфейл</a>
  72                <button class="btn btn-outline" id="copy-invoice" data-pr={view.payment.pr}>Копирай</button>
  73              </div>
  74            </div>
  75          )}
  76          {view.payment_method === 'onchain' && view.status === 'pending' && view.payment?.checkoutUrl && (
  77            <p><a class="btn btn-primary" href={view.payment.checkoutUrl}>Плати с Bitcoin (BTCPay)</a></p>
  78          )}
  79          {view.payment_status === 'paid' && (
  80            <p>✓ Плащането е получено.</p>
  81          )}
  82        </div>
  83  
  84        <p>Ще получите потвърждение на имейл.</p>
  85        <a href="/shop" class="btn btn-primary">Продължете да пазарувате</a>
  86      </div>
  87    </section>
  88  </Base>
  89  
  90  <script>
  91    // copy invoice
  92    document.getElementById('copy-invoice')?.addEventListener('click', (e) => {
  93      const pr = (e.currentTarget as HTMLElement).dataset.pr!;
  94      navigator.clipboard?.writeText(pr);
  95    });
  96  
  97    // poll payment status for methods that settle asynchronously
  98    const area = document.getElementById('payment-area')!;
  99    const method = area.dataset.method!;
 100    const status = area.dataset.status!;
 101    if (status === 'pending' && ['stripe', 'lightning', 'onchain'].includes(method)) {
 102      const orderId = area.dataset.order!;
 103      const token = area.dataset.token!;
 104      const poll = async () => {
 105        try {
 106          const r = await fetch(`/api/orders/${orderId}?t=${token}`);
 107          if (r.ok) {
 108            const o = await r.json();
 109            if (o.payment_status === 'paid') {
 110              location.reload();
 111              return;
 112            }
 113          }
 114        } catch {}
 115        setTimeout(poll, 5000);
 116      };
 117      setTimeout(poll, 5000);
 118    }
 119  </script>
 120  
 121  <style>
 122    .order-id { font-family: monospace; font-size: 0.8em; color: var(--text-light); }
 123    .order-status { margin-bottom: 1.5rem; }
 124    .order-items { max-width: 480px; margin-bottom: 2rem; }
 125    .order-item { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }
 126    .order-total { border-bottom: none; font-size: 1.1rem; }
 127    #payment-area { margin-bottom: 2rem; }
 128    .invoice-code { display: block; word-break: break-all; background: var(--bg, #f5f5f5); padding: 0.75rem; border-radius: 8px; font-size: 0.8rem; max-width: 640px; }
 129  </style>
 130